﻿//
//  THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
//  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
//  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
//  PURPOSE.
//
//  License: GNU General Public License (GPLv2)
//
//  Email: pavel_torgashov@mail.ru.
//
//  Copyright (C) Pavel Torgashov, 2012. 

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Text;
using OpenCLTemplate;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Collections;

namespace CLEditor
{
    public static class Compiler
    {
        public static bool Compile(string text, out List<ErrorItem> logs)
        {
            logs = new List<ErrorItem>();
            List<string> temp = null;

            CLCalc.InitCL();
            List<Cloo.ComputeDevice> L = CLCalc.CLDevices;
            CLCalc.Program.DefaultCQ = 0;
            bool res = true;

            try
            {
                CLCalc.Program.Compile(text, out temp);
            }
            catch (NullReferenceException)
            {
                throw new Exception("OpenCL is not installed.");
            }
            catch (Exception ex)
            {
                if (temp == null || temp.Count == 0)
                    throw;
                res = false;
            }

            if (temp!=null && temp.Count > 0)
            {
                var items = temp[0].Split(new string[] { "\n\n" }, StringSplitOptions.RemoveEmptyEntries);
                foreach (var item in items)
                {
                    var err = ErrorItem.Parse(item);
                    if(err!=null)
                        logs.Add(err);
                }

                logs.Sort((i1, i2)=>
                    {
                        int comp = i1.type.CompareTo(i2.type);
                        if (comp == 0)
                            comp = i1.line.CompareTo(i2.line);
                        return comp;
                    });
            }

            return res;
        }

        public static bool CompileAndRun(string text, ExplorerItem entryPoint, List<string> argValues, int[] globalWorkSizes, out List<ErrorItem> logs, out string result)
        {
            result = "";
            text += "\r\n\r\n" + BuildEntryFunction(entryPoint);

            if (!Compile(text, out logs))
                return false;

            List<CLCalc.Program.Variable> vars = new List<CLCalc.Program.Variable>();
            //создаем переменные
            for(int i=0;i<entryPoint.args.Count;i++)
                vars.Add(BuildVar(entryPoint.args[i], argValues[i]));
            //
            if(entryPoint.funcType.BaseType != "void")
                vars.Add(BuildVar(new FunctionArgument() { name = "Result", type = entryPoint.funcType }, BuildDefaultValue(entryPoint.funcType)));

            //Присовоение названия скомпилированной программе, её загрузка.
            CLCalc.Program.Kernel kernel = new CLCalc.Program.Kernel("FakeEntryPoint");

            Stopwatch sw = Stopwatch.StartNew();
            //Исполняем ядро VectorSum с аргументами args и колличеством потоков workers
            kernel.Execute(vars.ToArray(), globalWorkSizes);
            sw.Stop();
            StringBuilder sb = new StringBuilder("Execute time: " + sw.Elapsed.ToString()+"\r\n");

            //выгружаем из памяти
            for (int i = 0; i < entryPoint.args.Count; i++)
            if(entryPoint.args[i].type.IsPointer)
                sb.AppendLine(entryPoint.args[i].type + " " + entryPoint.args[i].name + ": " + ReadFromVar(vars[i], entryPoint.args[i]));
            if (entryPoint.funcType.BaseType != "void")
                sb.AppendLine(entryPoint.funcType.ToString() + " "+entryPoint.funcName+": " + ReadFromVar(vars[entryPoint.args.Count], new FunctionArgument() { name = "Result", type = entryPoint.funcType }));


            result = sb.ToString();

            return true;
        }

        private static CLCalc.Program.Variable BuildVar(FunctionArgument arg, string values)
        {
            Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
            string[] array = values.Split(new char[]{';'}, StringSplitOptions.None);
            List<double> list = new List<double>();
            foreach (var var in array)
            {
                var parts = var.Trim(' ', ')', '(').Split(',');
                if (parts.Length != arg.type.Dim)
                    throw new Exception(string.Format("Can't parse '{0}' to type '{1}'", var, arg.type.BaseType + (arg.type.Dim > 1 ? arg.type.Dim .ToString(): "")));
                foreach (var item in parts)
                    list.Add(double.Parse(item.Trim()));
            }

            CLCalc.Program.Variable result = null;

            switch (arg.type.BaseType)
            {
                case "double":
                    result = new CLCalc.Program.Variable(list.ToArray()); break;
                case "float":
                    result = new CLCalc.Program.Variable(list.ConvertAll<float>((x)=>(float)x).ToArray()); break;
                case "int":
                    result = new CLCalc.Program.Variable(list.ConvertAll<int>((x)=>(int)x).ToArray()); break;
                case "uint":
                    result = new CLCalc.Program.Variable(list.ConvertAll<int>((x) => (int)(uint)x).ToArray()); break;
                case "long":
                    result = new CLCalc.Program.Variable(list.ConvertAll<long>((x) => (long)x).ToArray()); break;
                case "ulong":
                    result = new CLCalc.Program.Variable(list.ConvertAll<long>((x) => (long)(ulong)x).ToArray()); break;
                default:
                    result = new CLCalc.Program.Variable(list.ConvertAll<byte>((x) => (byte)(int)x).ToArray()); break;
            }

            return result;
        }

        private static string ReadFromVar(CLCalc.Program.Variable var, FunctionArgument arg)
        {
            Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;

            IList array = null;
            
            switch (arg.type.BaseType)
            {
                case "double":
                    array = new double[var.OriginalVarLength];
                    var.ReadFromDeviceTo(array as double[]);
                    break;
                case "float":
                    array = new float[var.OriginalVarLength];
                    var.ReadFromDeviceTo(array as float[]);
                    break;
                case "int":
                case "uint":
                    array = new int[var.OriginalVarLength];
                    var.ReadFromDeviceTo(array as int[]);
                    break;
                case "long":
                case "ulong":
                    array = new long[var.OriginalVarLength];
                    var.ReadFromDeviceTo(array as long[]);
                    break;
                default:
                    array = new byte[var.OriginalVarLength];
                    var.ReadFromDeviceTo(array as byte[]);
                    break;
            }

            StringBuilder sb = new StringBuilder();

            int count = var.OriginalVarLength / arg.type.Dim;
            for (int i = 0; i < count; i++)
            {
                string s = "";
                for (int j = 0; j < arg.type.Dim; j++)
                    s += array[i * arg.type.Dim + j].ToString() + ", ";
                sb.Append("("+s.TrimEnd(' ',',')+"); ");
            }

            var result = sb.ToString();
            result = result.TrimEnd(';', ' ');
            if (arg.type.Dim == 1)
                result = result.Trim('(', ')');

            return result;
        }

        public static string BuildEntryFunction(ExplorerItem func)
        {
            string argList = "";
            string paramList = "";

            foreach (var a in func.args)
            {
                if (a.type.IsPointer)
                {
                    argList += string.Format("__global {0} {1}, ", a.type, a.name);
                    paramList += string.Format("{0}, ", a.name);
                }
                else
                {
                    argList += string.Format("__global {0}* {1}, ", a.type, a.name);
                    paramList += string.Format("{0}[0], ", a.name);
                }
            }

            if (func.funcType.ToString() != "void")
                argList += string.Format("__global {0}* fakeResult, ", func.funcType);

            argList = argList.TrimEnd(',', ' ');
            paramList = paramList.TrimEnd(',', ' ');

            string result = "";
            if (func.funcType.ToString() != "void")
                result = string.Format(@"__kernel void FakeEntryPoint({0})
                {{
                   fakeResult[0] = {1}({2});
                }}", argList, func.funcName, paramList);
                else
                result = string.Format(@"__kernel void FakeEntryPoint({0})
                {{
                   {1}({2});
                }}", argList, func.funcName, paramList);

            return result;
        }

        public static string BuildDefaultValue(CLType type)
        {
            string val = "0";
            if (type.BaseType.Contains("float") || type.BaseType.Contains("double"))
                val = "0.0";

            string result = "";
            for (int i = 0; i < type.Dim; i++)
                result += val + ", ";
            result = result.TrimEnd(',', ' ');
            if (type.Dim > 1)
                result = "(" + result + ")";
            if (type.IsPointer)
                result = string.Format("{0}; {0}; {0}", result);

            return result;
        }
    }

    public class ErrorItem
    {
        public ErrorItemType type;
        public int line;
        public int pos;
        public string desc;

        public static ErrorItem Parse(string text)
        {
            ErrorItem result = new ErrorItem();
            var m = Regex.Match(text, @"^.+\((?<line>\d+)\):\s*(?<type>\w+):(?<desc>.*)\n(?<source>[^\n]+)(?<pos>\n\s*\^\s*)$", RegexOptions.Singleline);
            if (!m.Success)
                return null;
            result.desc = m.Groups["desc"].Value.Replace('\n', ' ').Replace("\t", " ").Trim();
            while (result.desc.Contains("  "))
                result.desc = result.desc.Replace("  ", " ");
            result.line = int.Parse(m.Groups["line"].Value);
            result.pos = m.Groups["pos"].Value.Length - 3;
            result.type = m.Groups["type"].Value == "error" ? ErrorItemType.Error : ErrorItemType.Warning;

            return result;
        }
    }

    public enum ErrorItemType
    {
        Error, Warning
    }
}
